import csv
import datetime as dtm
import logging
import os.path
import pickle
import time
import urllib

from datetime import timedelta
from socket import *
from threading import Timer
from time import sleep

import config as c
import Zeroconf as Z

log = logging.getLogger('TiVoToDo.tools')

##########################
# TiVo related functions #
##########################
def isHDtivo(tsn):
    """Return whether a given tsn is an HD TiVo."""
    return bool(tsn and tsn[0] >= '6' and tsn[:3] != '649')

def tivo_tuners(tivo,               # rdl[10],  TiVo name
                duration,           # rdl[3],  Total duration
                delta,              # rdl[7],  Duration to subtract
                show,               # rdl[12], Show title
                start_prev,         # Previous record time match
                start_next,         # Next record time match
                show_start):        # Start of show flag
    """Use a dictionary to record tuner's activity."""
    log = logging.getLogger('TiVoToDo.tools.tivo_tuners')
    return_string = []
    log.debug('start_prev = %s, start_next = %s' % (start_prev, start_next))
    log.debug('Before = %s' % c.tuners)
    offset = c.offset[1]
    duration += offset

    def init_tuners(tivo):
        # Initialize an empty record for "tuner 1".
        if c.tuners[tivo][2] <= 0:
            c.tuners[tivo][0] = c.free_tuner
            c.tuners[tivo][1] = 0
            c.tuners[tivo][2] = 0
            c.tuners[tivo][3] = 0
            c.tuners[tivo][4] = c.free_tuner
        # Initialize an empty record for "tuner 2".
        if c.tuners[tivo][7] <= 0:
            c.tuners[tivo][5] = c.free_tuner
            c.tuners[tivo][6] = 0
            c.tuners[tivo][7] = 0
            c.tuners[tivo][8] = 0
            c.tuners[tivo][9] = c.free_tuner

    def tuner_one(tivo, duration, delta, show):
        # Update tuner one with a show's info.
        c.tuners[tivo][0] = tivo
        c.tuners[tivo][1] = duration
        if show_start:
            c.tuners[tivo][2] = duration
        c.tuners[tivo][3] = delta
        c.tuners[tivo][4] = show

    def tuner_two(tivo, duration, delta, show):
        # Update tuner two with a show's info.
        c.tuners[tivo][5] = tivo
        c.tuners[tivo][6] = duration
        if show_start:
            c.tuners[tivo][7] = duration
        c.tuners[tivo][8] = delta
        c.tuners[tivo][9] = show

    for k in c.tivos.iterkeys():
        # Single tuner active.
        if not start_prev and not start_next:
            log.debug('start_prev = False, start_next = False')
            c.tuners[k][2] -= c.tuners[k][3]
            c.tuners[k][7] -= c.tuners[k][8]
            init_tuners(k)
        # Multi-tuner active.
        if not start_prev and start_next:
            log.debug('start_prev = False, start_next = True')
            c.tuners[k][2] -= c.tuners[k][3]
            c.tuners[k][7] -= c.tuners[k][8]
            init_tuners(k)

        # Don't alter any other TiVo's records.
        if k == tivo:
            # Tuner one has the same show.
            if c.tuners[k][4] == show:
                log.debug('Match on tuner one')
                tuner_one(k, duration, delta, show)
            # Tuner two has the same show.
            elif c.tuners[k][9] == show:
                log.debug('Match on tuner two')
                tuner_two(k, duration, delta, show)
            # All tuners are free.
            elif c.tuners[k][1] == 0 and c.tuners[k][6] == 0:
                log.debug('Both tuners free')
                tuner_one(k, duration, delta, show)
                c.tuners[k][2] = duration
            # Tuner one is free.
            elif c.tuners[k][1] == 0:
                log.debug('Tuner one is free')
                tuner_one(k, duration, delta, show)
                c.tuners[k][2] = duration
            # Tuner two is free.
            elif c.tuners[k][6] == 0:
                log.debug('Tuner two is free')
                tuner_two(k, duration, delta, show)
                c.tuners[k][7] = duration
            # Tuner one has tail of show.
            elif c.tuners[k][2] < 5 and show_start:
                log.debug('Tuner one being overlayed')
                tuner_one(k, duration, delta, show)
                c.tuners[k][2] = duration
            # Tuner two has tail of show.
            elif c.tuners[k][7] < 5 and show_start:
                log.debug('Tuner two being overlayed')
                tuner_two(k, duration, delta, show)
                c.tuners[k][7] = duration

        # Nullify any names for free tuners.
        if c.tuners[k][1] == 0:
            dur1 = ''
        else:
            dur1 = ' (%s)' % str(c.tuners[k][1] - offset)
        if c.tuners[k][6] == 0:
            dur2 = ''
        else:
            dur2 = ' (%s)' % str(c.tuners[k][6] - offset)

        return_string.extend([(c.tuners[k][4] + dur1),
                              (c.tuners[k][9] + dur2)])

    log.debug('Returned = %s' % return_string)
    log.debug('After  = %s\n' % c.tuners)
    return return_string

###############################
# Date/Time related functions #
###############################
def date2str(date_obj):
    """Return a string from a datetime object."""
    year = date_obj.strftime("%Y%m/%d%I:%M %p")[:4]
    MMdd = date_obj.strftime("%Y%m/%d%I:%M %p")[4:9]
    time = date_obj.strftime("%Y%m/%d%I:%M %p")[9:]
    return(year, MMdd, time)

def run_time():
    """Establish persistent date/time variables."""
    c.now = dtm.datetime.now()
    c.this_day = c.now.day
    c.this_month = c.now.month
    c.this_year = str(c.now.year)

def str2date(year, MMdd, time):
    """Return a datetime object from a TiVo date and time."""
    # Account for year not being provided.
    if len(year) == 4:
        date_obj = dtm.datetime.strptime(year + MMdd + time, "%Y%m/%d%I:%M %p")
    # A two digit year has been provided.
    else:
        date_obj = dtm.datetime.strptime(year + MMdd + time, "%y%m/%d%I:%M %p")
    return(date_obj)

def time_table(year, month, day):
    """Create a 24 hour time table based on date."""
    # Establish a 'current' working date object.
    today = dtm.datetime(year, month, day)
    # Establish a 'future' working date object.
    tomorrow = today + dtm.timedelta(days=1)
    time_table = []
    # Loop through today's date, creating 30 minute entries.
    while today < tomorrow:
        time_table.append(today)
        today += dtm.timedelta(minutes=30)
    return(time_table)

def check_time(dt_obj):
    """Return an index of a record's time."""
    log = logging.getLogger('TiVoToDo.tools.check_time')
    # Create a time table on date object's date.
    tt = time_table(dt_obj.year,
                    dt_obj.month,
                    dt_obj.day)
    # Expand time table to include the previous 30 minutes.
    tt.insert(0, tt[0] - dtm.timedelta(minutes=30))
    # Expand time table to include the next 30 minutes.
    tt.append(tt[len(tt)-1] + dtm.timedelta(minutes=30))
    # Loop through the 25 hour time table.
    for i in range(len(tt)):
        # The adjusted date object falls within first 30 minutes.
        if i == 0 and dt_obj >= tt[i] and dt_obj < tt[i+1]:
            return(i)
        # The adjusted date object falls within last 30 minutes.
        elif i == len(tt) - 1 and dt_obj >= tt[i]:
            return(i)
        # The adjusted date object falls somewhere inbetween.
        elif dt_obj >= tt[i] and dt_obj < tt[i+1]:
            return(i)

def update_rdl(duration):
    """Add entries to the Raw Data List."""
    week = ['Mon', 'Tue',  'Wed',  'Thu',  'Fri',  'Sat',  'Sun']
    log = logging.getLogger('TiVoToDo.tools.update_rdl')
    # Establish clipping offset.
    offset = dtm.timedelta(minutes = c.offset[1])
    # Create a 30 minute time object
    thirty = dtm.timedelta(minutes = 30)
    # Determine the show's end time
    et = c.rdi_dttm + dtm.timedelta(minutes = duration)
    log.debug('et = %s' % str(c.rdi_dttm + dtm.timedelta(minutes = duration)))
    if c.offset[0] == 'Enabled':
        # Calculate the rounded duration time.
        dur_range = range(int(round((duration + .5) / 30, 0)))
        log.debug('range = %s' % len(dur_range))
        # Account for short airing shows
        if len(dur_range) < 1:
            dur_range = range(1)
        # Loop through the duration, adding an entry for every 30 minutes.
        for i in dur_range:
            # Calculate the show's logical start time.
            st = c.rdi_dttm + (thirty * i)
            log.debug('st = %s, et = %s' % (st, et))
            # Create a row containting the show's information.
            c.rdl.append([date2str(st)[1],          # [0] MM/dd
                          week[st.weekday()],       # [1] Weekday
                          date2str(st)[2],          # [2] hh:mm am/pm
                          c.rdi_dura,               # [3] Show duration
                          i,                        # [4] Actual starttime
                          st,                       # [5] Start datetime obj
                          et,                       # [6] End datetime obj
                          30,                       # [7] Current duration
                          st - offset,              # [8] Clipped starttime
                          et - offset,              # [9] Clipped endtime
                          c.rdi_tivo,               # [10] TiVo name
                          c.rdi_lchn,               # [11] Channel name
                          c.rdi_titl,               # [12] Show title
                          c.rdi_rtyp,               # [13] Recording type
                          c.rdi_skey,               # [14] Dict key
                          c.rdi_ttsn,               # [15] TiVo TSN
                          c.rdi_schd,               # [16] Show schedule
                          c.rdi_new])               # [17] New Show indicator
    # Strictly enforce the total airtime minutes.
    else:
        # Create a 24 hour time table based on the date.
        tt = time_table(et.year,
                        et.month,
                        et.day)
        # Pad thirty minutes before.
        tt.insert(0, tt[0] - thirty)
        # Pad thirty minutes after.
        tt.append(tt[len(tt)-1] + thirty)
        # Set the show's logical start time.
        st = c.rdi_dttm
        # Initialize start time indicator.
        i = 0
        # Loop through the total duration of the show.
        while duration > 0:
            # Get the time of the next half hour after the show starts.
            where = check_time(st) + 1
            # 
            remaining = int(str(tt[where] - st).split(':')[1])
            if check_time(st) == check_time(et):
                remaining = int(str(et - st).split(':')[1])
            log.debug('st = %s, et = %s, remaining = %s' % (st, et, remaining))
            c.rdl.append([date2str(st)[1],          # [0] MM/dd
                          week[st.weekday()],       # [1] Weekday
                          date2str(st)[2],          # [2] hh:mm am/pm
                          c.rdi_dura,               # [3] Show duration
                          i,                        # [4] Actual starttime
                          st,                       # [5] Start datetime obj
                          et,                       # [6] End datetime obj
                          remaining,                # [7] Current duration
                          st,                       # [8] Clipped starttime
                          et,                       # [9] Clipped endtime
                          c.rdi_tivo,               # [10] TiVo name
                          c.rdi_lchn,               # [11] Channel name
                          c.rdi_titl,               # [12] Show title
                          c.rdi_rtyp,               # [13] Recording type
                          c.rdi_skey,               # [14] Dict key
                          c.rdi_ttsn,               # [15] TiVo TSN
                          c.rdi_schd,               # [16] Show schedule
                          c.rdi_new])               # [17] New Show indicator
            st += dtm.timedelta(minutes=remaining)
            i += 1
            duration -= remaining
    log.debug('c.rdi_new = %s' % c.rdi_new)
    c.rdi_new = False
    

#########################
# Yet to be implemented #
#########################
def hyperlink(spol, sdtl, edtl, tsn, tivo):
    """Create Season Pass url for use in an Excel "=HYPERLINK()" function."""
    # spos = spos.split('-')
    # spos = datetime.datetime(int(spos[0]), int(spos[1]), int(spos[2]),
                             # int(spos[3]), int(spos[4]), int(spos[5]))
    # now = datetime.datetime.now()
    # week = datetime.timedelta(days = 7)
    # while spos < now:
        # spos + week
    # spos = spos.strftime("%Y, %m, %d, %H, %M, %S")
    rec_opts = c.SP_opts()
    rec_opts['offerId'] = spol
    rec_opts['collectionId'] = sdtl
    rec_opts['contentId'] = edtl
    rec_opts['tsn'] = tsn
    return(c.base_url + c.spwo_url + urllib.urlencode(rec_opts))

##########################
# File related functions #
##########################
def make_csv(obj, file):
    """Create a .csv file from a given object."""
    output_path = c.p
    output_dir = c.output_dir
    output_file = os.path.join(output_path, output_dir, file)
    write_csv = csv.writer(open(output_file, "wb"), dialect='excel')
    write_csv.writerows(obj)

def dump_object(obj, file):
    """Create a pickle file from a given object."""
    obj_dir = c.obj_dir
    obj_file = os.path.join(obj_dir, file)
    file_obj = open(obj_file, 'wb')
    pickle.dump(obj, file_obj)

def load_object(file):
    """Load a pickle file."""
    obj_dir = c.obj_dir
    obj_file = os.path.join(obj_dir, file)
    file_obj = open(obj_file, 'rb')
    return(pickle.load(file_obj))

def read_cache():
    """Read back a dictionary of show information."""
    cache_path = c.p
    cache_file = c.cache_file
    if os.path.exists(cache_file) and os.path.isfile(cache_file):
        cache = open(cache_file, 'rb')
        c.shows = pickle.load(cache)

def write_cache():
    """Write the show information dictionary to a file."""
    cache_path = c.p
    cache_file = c.cache_file
    cache = open(cache_file, 'wb')
    pickle.dump(c.shows, cache)

def html_debug():
    """Write errant html to a file."""
    debug_path = c.p
    debug_file = c.html_file
    debug = open(debug_file, 'wb')
    debug.write(c.result)

def xml_debug():
    """Write errant xml to a file."""
    debug_path = c.p
    debug_file = c.xml_file
    debug = open(debug_file, 'wb')
    debug.write(c.result)

#############################
# LAN/WAN related functions #
#############################
def login(target_url, orig_url, email, password):
    """Create the url required for logging into tivo.com."""
    url = target_url + '/tivo-tco/login.do'
    login_data = urllib.urlencode({
        'cams_security_domain': 'tivocom',
        'cams_login_config': 'http',
        'cams_original_url': orig_url,
        'cams_cb_username': email,
        'cams_cb_password': password
    })
    return url, login_data

class ZCListener:
    def __init__(self, names):
        self.names = names

    def removeService(self, server, type, name):
        if name in self.names:
            self.names.remove(name)

    def addService(self, server, type, name):
        self.names.append(name)

class ZCBroadcast:
    def __init__(self):
        self.rz = Z.Zeroconf()
        self.log = logging.getLogger("TiVoToDo.tools.ZCBroadcast")

    def scan(self):
        """ Look for TiVos using Zeroconf. """
        VIDS = '_tivo-videos._tcp.local.'
        names = []

        # Get the names of servers offering TiVo videos.
        browser = Z.ServiceBrowser(self.rz, VIDS, ZCListener(names))

        # Give them half a second to respond.
        time.sleep(.5)

        # Now get the addresses -- this is the slow part.
        for name in names:
            info = self.rz.getServiceInfo(VIDS, name)
            if info and 'TSN' in info.properties:
                tsn = info.properties['TSN']
                name = name.replace('.' + VIDS, '').encode('utf-8')
                if isHDtivo(tsn):
                    c.tivos[name] = tsn

        self.log.info(' ...and found %s: %s' % (len(c.tivos),
                                                ' '.join(c.tivos.keys())))

    def shutdown(self):
        self.rz.close()
        time.sleep(.5)

class Beacon:
    UDPSock = socket(AF_INET, SOCK_DGRAM)
    UDPSock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
    services = []

    def __init__(self):
        self.log = logging.getLogger("TiVoToDo.tools.Beacon")
        self.log.info('Scanning for TiVos...')
        self.bd = ZCBroadcast()
        self.bd.scan()

    def format_services(self):
        return ';'.join(self.services)

    def format_beacon(self, conntype, services=True):
        beacon = ['tivoconnect=1',
                  'swversion=1',
                  'method=%s' % conntype,
                  'identity=%s' % c.getGUID(),
                  'machine=%s' % gethostname(),
                  'platform=pc']

        if services:
            beacon.append('services=' + self.format_services())
        else:
            beacon.append('services=TiVoMediaServer:0/http')

        return '\n'.join(beacon)

    def send_beacon(self):
        beacon_ip = c.get_broadcast()
        if not beacon_ip:
            beacon_ip = '255.255.255.255'
        try:
            self.UDPSock.sendto(self.format_beacon('broadcast'),
                                    (beacon_ip, 2190))
        except error, e:
            log.debug(e)

    def start(self):
        self.send_beacon()
        self.timer = Timer(60, self.start)
        self.timer.start()

    def stop(self):
        self.timer.cancel()
        if self.bd:
            self.bd.shutdown()

